use std::fs::{self, File};
use std::io::{Seek, SeekFrom, self, Read};
use std::net::{TcpStream, ToSocketAddrs};

use native_tls::{Certificate, TlsConnector, TlsStream};

use crate::util::{StreamWrapper, StreamError, self};
use crate::constant::*;

impl From<StreamError> for String {
    fn from(_: StreamError) -> Self {
        "Network error".to_owned()
    }
}

fn load_cert(path: &str) -> Result<Certificate, String> {
    let buf = fs::read(path)
        .map_err(|e| format!("Failed to read crt file: {e}"))?;

    Certificate::from_pem(&buf)
        .map_err(|e| format!("Failed to create cert: {e}"))
}

pub struct Connection {
    stream: StreamWrapper<TlsStream<TcpStream>>
}

impl Connection {
    pub fn connect(addr: impl ToSocketAddrs, domain: &str, crt_path: Option<&str>, username: &str, password: &str, register: bool) -> Result<Connection, String> {
        let connector = if let Some(path) = crt_path {
            TlsConnector::builder().add_root_certificate(load_cert(path)?).build()
        } else {
            TlsConnector::new()
        }.map_err(|e| format!("Failed to create connector: {e}"))?;

        let stream = TcpStream::connect(addr)
            .map_err(|e| format!("Failed to connect: {e}"))?;

        let stream = connector.connect(domain, stream)
            .map_err(|e| format!("Failed to conect: {e}"))?;
        let mut stream = StreamWrapper(stream);

        stream.send_u8(if register { CMD_AUTH_REG } else { CMD_AUTH_LOGIN })?;
        stream.send_buf8(username.as_bytes())?;
        stream.send_buf8(password.as_bytes())?;

        if stream.recv_u8()? == 1 {
            Ok(Connection { stream })
        } else {
            Err(if register { "User exists" } else { "Invalid username or password" }.to_owned())
        }
    }

    pub fn fetch_list(&mut self, part: bool) -> Result<Vec<(u64, String)>, String> {
        self.stream.send_u8(if part { CMD_LIST_UPLOADING } else { CMD_LIST_FINISHED })?;

        let len = self.stream.recv_u64()?;

        let mut list = vec![];

        for _ in 0..len {
            list.push((self.stream.recv_u64()?, self.stream.recv_str8()?));
        }

        Ok(list)
    }

    pub fn upload_file(&mut self, name: &str, offset: Option<u64>, path: &str) -> Result<bool, String> {
        let mut file = File::open(path)
            .map_err(|e| format!("Failed to open file: {e}"))?;

        let meta = file.metadata()
            .map_err(|e| format!("Failed to get file meta: {e}"))?;
        let len = meta.len();

        if let Some(offset) = offset {
            if len < offset {
                return Err(format!("Failed to upload: offset ({offset}) > file len ({len})"))
            }
        }

        let b_name = name.as_bytes();

        if let None = offset {
            self.stream.send_u8(CMD_BEGIN_UPLOAD)?;
            self.stream.send_buf8(b_name)?;
        }

        let offset = offset.unwrap_or(0);

        if offset < len {
            self.stream.send_u8(CMD_UPLOAD)?;
            self.stream.send_buf8(b_name)?;
            self.stream.send_u64(offset)?;
            self.stream.send_u64(meta.len() - offset)?;
            file.seek(SeekFrom::Start(offset))
                .map_err(|e| format!("Failed to read file: {e}"))?;
            io::copy(&mut file, &mut self.stream.0)
                .map_err(|e| format!("Failed to send file: {e}"))?;
        }

        self.stream.send_u8(CMD_END_UPLOAD)?;
        self.stream.send_buf8(b_name)?;
        file.seek(SeekFrom::Start(0))
            .map_err(|e| format!("Failed to read file: {e}"))?;
        let hash = util::stream_hash(&mut file)
            .map_err(|e| format!("Failed to read file: {e}"))?;
        self.stream.send(&hash)?;

        Ok(self.stream.recv_u8()? == 1)
    }

    pub fn download_file(&mut self, name: &str, offset: u64, len: u64, path: &str) -> Result<(), String> {
        self.stream.send_u8(CMD_DOWNLOAD)?;
        self.stream.send_buf8(name.as_bytes())?;
        self.stream.send_u64(offset)?;
        self.stream.send_u64(len)?;

        let mut file = File::options()
            .write(true)
            .create(offset == 0)
            .truncate(offset == 0)
            .append(offset != 0)
            .open(path)
            .map_err(|e| format!("Failed to create file: {e}"))?;

        if offset != 0 {
            let file_len = file.metadata()
                .map_err(|e| format!("Failed to get file meta: {e}"))?
                .len();
            if file_len != offset {
                return Err(format!("Invalid file size: expected {offset}, got {file_len}"));
            }
        }

        let mut take = Read::by_ref(&mut self.stream.0).take(len);
        io::copy(&mut take, &mut file)
            .map_err(|e| format!("Failed to receive file: {e}"))?;

        Ok(())
    }

    pub fn get_hash(&mut self, name: &str) -> Result<Vec<u8>, String> {
        self.stream.send_u8(CMD_GET_HASH)?;
        self.stream.send_buf8(name.as_bytes())?;

        let mut buf = vec![0; 32];
        self.stream.recv(&mut buf)?;

        Ok(buf)
    }
}
